﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ContentTypesManager.cs" company="CodePlex">
//   The MIT License
//   Copyright (c) 2013 Julien Blin
//   
//   Permission is hereby granted, free of charge, to any person obtaining a copy
//   of this software and associated documentation files (the "Software"), to deal
//   in the Software without restriction, including without limitation the rights
//   to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//   copies of the Software, and to permit persons to whom the Software is
//   furnished to do so, subject to the following conditions:
//   
//   The above copyright notice and this permission notice shall be included in
//   all copies or substantial portions of the Software.
//   
//   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//   OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//   THE SOFTWARE.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace UCodeFirst.ContentTypes
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;

    using umbraco.cms.businesslogic.web;

    using Umbraco.Core;
    using Umbraco.Core.Logging;
    using Umbraco.Core.Models;

    /// <summary>
    /// Manager responsible for synchronizing content types.
    /// </summary>
    public class ContentTypesManager : UCodeFirstManagerBase
    {
#pragma warning disable 612,618

        /// <summary>
        /// The key to store and retrieve the list of synchronized content type aliases.
        /// </summary>
        private const string ContentTypesListKey = "ContentTypesList";

        /// <summary>
        /// The synchronized content types.
        /// </summary>
        private List<SynchroContentType> synchroContentTypes;

        /// <summary>
        /// Returns <see cref="ManagerPriorities.ContentTypes"/>.
        /// </summary>
        public override int Priority
        {
            get
            {
                return ManagerPriorities.ContentTypes;
            }
        }

        /// <summary>
        /// All the managers <see cref="UCodeFirstManagerBase.Synchronize"/> methods will run after all the managers <see cref="UCodeFirstManagerBase.BeforeSynchronization"/> method have run,
        /// <b>only</b> if the synchronization happens.<br />
        /// They run in the order specified by the <see cref="UCodeFirstManagerBase.Priority"/>.
        /// </summary>
        /// <param name="httpApplication">
        ///     The <see cref="UmbracoApplicationBase"/>.
        /// </param>
        /// <param name="applicationContext">
        ///     The <see cref="ApplicationContext"/>.
        /// </param>
        public override void Synchronize(UmbracoApplicationBase httpApplication, ApplicationContext applicationContext)
        {
            this.synchroContentTypes = new List<SynchroContentType>();
            var contentTypeBaseTypes = TypesScanner.GetClassesInBinThatAreAssignableFrom<ContentTypeBase>().OrderByClassInheritanceLevel().ToList();
            LogHelper.Debug<ContentTypesManager>(Resources.FoundNContentTypeBase.InvariantFormat(contentTypeBaseTypes.Count));

            var contentTypeService = ApplicationContext.Current.Services.ContentTypeService;
            var allContentTypes = contentTypeService.GetAllContentTypes().ToList();

            foreach (var contentTypeBaseType in contentTypeBaseTypes)
            {
                try
                {
                    LogHelper.Debug<ContentTypesManager>(Resources.Synchronizing.InvariantFormat(contentTypeBaseType.Name));

                    var contentTypeAttribute = contentTypeBaseType.GetAttr<ContentTypeAttribute>()
                                            ?? new ContentTypeAttribute();

                    var parentContentTypeId = -1;
                    if (contentTypeBaseType.BaseType != typeof(ContentTypeBase))
                    {
                        Debug.Assert(contentTypeBaseType.BaseType != null, "contentTypeBaseType.BaseType != null");
                        var parentContentType = allContentTypes.FirstOrDefault(x => x.Alias.InvariantEquals(contentTypeBaseType.BaseType.Name));
                        if (parentContentType == null)
                        {
                            throw new UCodeFirstException(Resources.UnableToFindContentType.InvariantFormat(contentTypeBaseType.BaseType.Name));
                        }

                        parentContentTypeId = parentContentType.Id;
                    }

                    var umbracoContentType = contentTypeService.GetContentType(contentTypeBaseType.Name)
                                          ?? new ContentType(parentContentTypeId);

                    var contentTypeUpdater = new ConditionalPropertyUpdater<IContentType>(umbracoContentType);
                    contentTypeUpdater.Update(x => x.Alias, contentTypeBaseType.Name);
                    contentTypeUpdater.Update(x => x.Name, contentTypeAttribute.GetName(contentTypeBaseType.Name));
                    contentTypeUpdater.Update(x => x.ParentId, parentContentTypeId);
                    contentTypeUpdater.Update(x => x.Icon, contentTypeAttribute.Icon);
                    contentTypeUpdater.Update(x => x.Thumbnail, contentTypeAttribute.Thumbnail);
                    contentTypeUpdater.Update(x => x.Description, contentTypeAttribute.Description);
                    contentTypeUpdater.Update(x => x.AllowedAsRoot, contentTypeAttribute.AllowedAsRoot);

                    if (contentTypeUpdater.HasBeenChanged)
                    {
                        contentTypeService.Save(umbracoContentType, UCodeFirstConfig.GetUser().Id);
                    }

                    if (!allContentTypes.Contains(umbracoContentType))
                    {
                        allContentTypes.Add(umbracoContentType);
                    }

                    this.synchroContentTypes.Add(new SynchroContentType { ContentTypeBase = contentTypeBaseType, Id = umbracoContentType.Id });

                    LogHelper.Debug<ContentTypesManager>(Resources.Synchronized.InvariantFormat(contentTypeBaseType.Name));
                }
                catch (Exception ex)
                {
                    throw new UCodeFirstException(Resources.ErrorWhileSynchronizing.InvariantFormat(contentTypeBaseType), ex);
                }
            }

            this.FixMasterContentTypeBugInNewApi();
            this.SynchronizeTabs();
            this.SynchronizeProperties();
            this.SetAllowedChildNodeTypesAndTemplates();

            LogHelper.Info<ContentTypesManager>(Resources.SynchronizedNContentTypes.InvariantFormat(contentTypeBaseTypes.Count));
        }

        /// <summary>
        /// All the managers <see cref="UCodeFirstManagerBase.AfterSynchronization"/> methods will run after all the managers <see cref="UCodeFirstManagerBase.Synchronize"/> method have run,
        /// no matter whether synchronization happens or not.<br />
        /// They run in the inverse order specified by the <see cref="UCodeFirstManagerBase.Priority"/>.
        /// </summary>
        /// <param name="httpApplication">
        ///     The <see cref="UmbracoApplicationBase"/>.
        /// </param>
        /// <param name="applicationContext">
        ///     The <see cref="ApplicationContext"/>.
        /// </param>
        /// <param name="synchronizationHappened">
        /// True if the synchronization happened.
        /// </param>
        public override void AfterSynchronization(UmbracoApplicationBase httpApplication, ApplicationContext applicationContext, bool synchronizationHappened)
        {
            if (synchronizationHappened)
            {
                Debug.Assert(this.synchroContentTypes != null, "this.synchroContentTypes != null");
                var synchronizedIds = this.synchroContentTypes.Select(x => x.Id).ToList();
                var contentTypeService = ApplicationContext.Current.Services.ContentTypeService;
                var previouslySynchronizedContentTypes = new List<string>();
                var previouslySynchronizedContentTypesValue = UCodeFirstData.GetValue<ContentTypesManager>(ContentTypesListKey);
                if (!string.IsNullOrEmpty(previouslySynchronizedContentTypesValue))
                {
                    previouslySynchronizedContentTypes = previouslySynchronizedContentTypesValue.Split(new[] { "|" }, StringSplitOptions.RemoveEmptyEntries).ToList();
                }

                var contentTypesToDelete = contentTypeService.GetAllContentTypes()
                                                            .Where(x => previouslySynchronizedContentTypes.InvariantContains(x.Alias)
                                                                          && !synchronizedIds.Contains(x.Id))
                                                            .ToList();

                foreach (var contentTypeToDelete in contentTypesToDelete)
                {
                    try
                    {
                        LogHelper.Info<ContentTypesManager>(Resources.Deleting.InvariantFormat(contentTypeToDelete.Name));
                        contentTypeService.Delete(contentTypeToDelete);
                    }
                    catch (Exception ex)
                    {
                        throw new UCodeFirstException(Resources.ErrorWhileDeleting.InvariantFormat(contentTypeToDelete.Name), ex);
                    }
                }

                UCodeFirstData.SetValue<ContentTypesManager>(ContentTypesListKey, string.Join("|", this.synchroContentTypes.Select(x => x.ContentTypeBase.Name)));
            }
        }

        /// <summary>
        /// The new API improperly position the master content type.
        /// </summary>
        private void FixMasterContentTypeBugInNewApi()
        {
            var allDocumentTypes = DocumentType.GetAllAsList();
            this.synchroContentTypes.ForEach(x => x.DocumentType = allDocumentTypes.First(y => y.Id == x.Id));

            foreach (var synchroContentType in this.synchroContentTypes)
            {
                if (synchroContentType.ContentTypeBase.BaseType == typeof(ContentTypeBase))
                {
                    synchroContentType.DocumentType.MasterContentType = 0;
                }
                else
                {
                    synchroContentType.DocumentType.MasterContentType = DocumentType.GetByAlias(synchroContentType.ContentTypeBase.BaseType.Name).Id;
                }
            }
        }

        /// <summary>
        /// Synchronizes the tabs.
        /// </summary>
        private void SynchronizeTabs()
        {
            foreach (var synchroContentType in this.synchroContentTypes)
            {
                try
                {
                    var hasBeenModified = false;
                    var propertyGroups = synchroContentType.ContentTypeBase.GetProperties()
                                                    .Where(x => x.HasAttr<PropertyAttribute>() && x.DeclaringType == synchroContentType.ContentTypeBase)
                                                    .OrderBy(x => x.GetAttr<PropertyAttribute>().GetGroupOrder())
                                                    .GroupBy(x => x.GetAttr<PropertyAttribute>().GetGroupName())
                                                    .Select(x => new { Name = x.Key, Order = x.First().GetAttr<PropertyAttribute>().GetGroupOrder() });

                    foreach (var propertyGroup in propertyGroups)
                    {
                        var tab = synchroContentType.DocumentType.getVirtualTabs.FirstOrDefault(t => t.Caption.InvariantEquals(propertyGroup.Name));
                        var tabId = 0;
                        if (tab == null)
                        {
                            tabId = synchroContentType.DocumentType.AddVirtualTab(propertyGroup.Name);
                            hasBeenModified = true;
                        }
                        else
                        {
                            tabId = tab.Id;
                        }

                        if ((tab == null) || (tab.SortOrder != propertyGroup.Order))
                        {
                            synchroContentType.DocumentType.SetTabSortOrder(tabId, propertyGroup.Order);
                            hasBeenModified = true;
                        }
                    }

                    var allVirtualTabsIncludingParents = synchroContentType.ContentTypeBase.GetProperties()
                                                                    .Where(x => x.HasAttr<PropertyAttribute>())
                                                                    .Select(x => x.GetAttr<PropertyAttribute>().GetGroupName())
                                                                    .Distinct()
                                                                    .ToList();
                    if (!allVirtualTabsIncludingParents.Contains(ContentTypeAttribute.DefaultValues.TabGenericProperties))
                    {
                        allVirtualTabsIncludingParents.Add(ContentTypeAttribute.DefaultValues.TabGenericProperties);
                    }

                    var tabsToRemove = synchroContentType.DocumentType.getVirtualTabs.Where(x => !allVirtualTabsIncludingParents.Contains(x.Caption));
                    foreach (var tabToRemove in tabsToRemove)
                    {
                        LogHelper.Debug<ContentTypesManager>(Resources.Deleting.InvariantFormat(tabToRemove.Caption));
                        synchroContentType.DocumentType.DeleteVirtualTab(tabToRemove.Id);
                        hasBeenModified = true;
                    }

                    if (hasBeenModified)
                    {
                        synchroContentType.DocumentType = DocumentType.GetByAlias(synchroContentType.DocumentType.Alias);
                    }
                }
                catch (Exception ex)
                {
                    throw new UCodeFirstException(Resources.ErrorWhileSynchronizing.InvariantFormat(synchroContentType.ContentTypeBase), ex);
                }
            }
        }

        /// <summary>
        /// Synchronize the properties.
        /// </summary>
        private void SynchronizeProperties()
        {
            foreach (var synchroContentType in this.synchroContentTypes)
            {
                try
                {
                    var sortOrder = 0;
                    foreach (var property in synchroContentType.ContentTypeBase.GetProperties().Where(x => x.HasAttr<PropertyAttribute>() && x.DeclaringType == synchroContentType.ContentTypeBase))
                    {
                        try
                        {
                            var propertyAttr = property.GetAttr<PropertyAttribute>();
                            var dataTypeDefinition = umbraco.cms.businesslogic.datatype.DataTypeDefinition.GetDataTypeDefinition(propertyAttr.GetDataTypeDefinition().Id);

                            var umbracoProperty = synchroContentType.DocumentType.getPropertyType(property.Name)
                                               ?? synchroContentType.DocumentType.AddPropertyType(dataTypeDefinition, property.Name, propertyAttr.GetName(property.Name));

                            var propertyUpdater = new ConditionalPropertyUpdater<umbraco.cms.businesslogic.propertytype.PropertyType>(umbracoProperty);

                            propertyUpdater.Update(x => x.DataTypeDefinition, dataTypeDefinition);
                            propertyUpdater.Update(x => x.Name, propertyAttr.GetName(property.Name));
                            propertyUpdater.Update(x => x.Mandatory, propertyAttr.Mandatory);
                            propertyUpdater.Update(x => x.ValidationRegExp, propertyAttr.Validation);
                            propertyUpdater.Update(x => x.Description, propertyAttr.Description);
                            propertyUpdater.Update(x => x.SortOrder, sortOrder);

                            if (propertyUpdater.HasBeenChanged)
                            {
                                umbracoProperty.Save();
                            }

                            ++sortOrder;

                            var tab = synchroContentType.DocumentType.getVirtualTabs.FirstOrDefault(t => t.Caption.InvariantEquals(propertyAttr.GetGroupName()));
                            if (tab == null)
                            {
                                throw new UCodeFirstException(Resources.InternalError);
                            }

                            synchroContentType.DocumentType.SetTabOnPropertyType(umbracoProperty, tab.Id);
                        }
                        catch (Exception ex)
                        {
                            throw new UCodeFirstException(Resources.ErrorWhileSynchronizing.InvariantFormat(property), ex);
                        }
                    }

                    var allPropertiesIncludingParents = synchroContentType.ContentTypeBase.GetProperties()
                                                                        .Where(x => x.HasAttr<PropertyAttribute>())
                                                                        .Select(x => x.Name);
                    var propertiesToRemove = synchroContentType.DocumentType.PropertyTypes.Where(x => !allPropertiesIncludingParents.Contains(x.Alias));
                    foreach (var propertyToRemove in propertiesToRemove)
                    {
                        LogHelper.Debug<ContentTypesManager>(Resources.Deleting.InvariantFormat(propertyToRemove.Name));
                        propertyToRemove.delete();
                    }
                }
                catch (Exception ex)
                {
                    throw new UCodeFirstException(Resources.ErrorWhileSynchronizing.InvariantFormat(synchroContentType.ContentTypeBase), ex);
                }
            }
        }

        /// <summary>
        /// Sets allowed child node types and templates.
        /// </summary>
        private void SetAllowedChildNodeTypesAndTemplates()
        {
            var contentTypeTemplateProviders =
                this.UCodeFirstApplicationEventHandler.Managers.Where(
                    x => x is IContentTypeTemplateProvider).Cast<IContentTypeTemplateProvider>().ToList();
            var fileService = ApplicationContext.Current.Services.FileService;
            var contentTypeService = ApplicationContext.Current.Services.ContentTypeService;
            var allContentTypes = contentTypeService.GetAllContentTypes().ToList();
            foreach (var contentTypeBase in this.synchroContentTypes.Select(x => x.ContentTypeBase))
            {
                var umbracoContentType = allContentTypes.First(x => x.Alias.InvariantEquals(contentTypeBase.Name));
                var contentTypeAttribute = contentTypeBase.GetAttr<ContentTypeAttribute>()
                                            ?? new ContentTypeAttribute();

                var hasBeenModified = false;
                if (contentTypeAttribute.AllowedChildNodes == null)
                {
                    if (umbracoContentType.AllowedContentTypes.Any())
                    {
                        umbracoContentType.AllowedContentTypes = Enumerable.Empty<ContentTypeSort>();
                        hasBeenModified = true;
                    }
                }
                else
                {
                    var allowedChildNodeList = new List<int>();
                    foreach (var allowedChildNode in contentTypeAttribute.AllowedChildNodes)
                    {
                        if (allowedChildNode == null)
                        {
                            continue;
                        }

                        var allowedChildNodeTyped = allowedChildNode as Type;
                        if (allowedChildNodeTyped != null)
                        {
                            foreach (var typesIncludingDescendant in this.synchroContentTypes.Where(x => allowedChildNodeTyped.IsAssignableFrom(x.ContentTypeBase) && !x.ContentTypeBase.IsAbstract).Select(x => x.ContentTypeBase))
                            {
                                var childContentType = contentTypeService.GetContentType(typesIncludingDescendant.Name);
                                if (childContentType == null)
                                {
                                    throw new UCodeFirstException(Resources.UnableToFindContentType.InvariantFormat(typesIncludingDescendant.Name));
                                }

                                allowedChildNodeList.Add(childContentType.Id);
                            }
                        }
                        else
                        {
                            var childContentType = allContentTypes.FirstOrDefault(x => x.Alias.InvariantEquals(allowedChildNode.ToString()));
                            if (childContentType == null)
                            {
                                throw new UCodeFirstException(Resources.UnableToFindContentType.InvariantFormat(allowedChildNode));
                            }

                            allowedChildNodeList.Add(childContentType.Id);
                        }

                        if (contentTypeAttribute.ForbiddenChildNodes != null)
                        {
                            foreach (var forbiddenBaseType in contentTypeAttribute.ForbiddenChildNodes)
                            {
                                foreach (var typesIncludingDescendant in this.synchroContentTypes.Where(x => forbiddenBaseType.IsAssignableFrom(x.ContentTypeBase)).Select(x => x.ContentTypeBase))
                                {
                                    var childContentType = allContentTypes.FirstOrDefault(x => x.Alias.InvariantEquals(typesIncludingDescendant.Name));
                                    if (childContentType == null)
                                    {
                                        throw new UCodeFirstException(Resources.UnableToFindContentType.InvariantFormat(typesIncludingDescendant.Name));
                                    }

                                    if (allowedChildNodeList.Contains(childContentType.Id))
                                    {
                                        allowedChildNodeList.Remove(childContentType.Id);
                                    }
                                }
                            }
                        }

                        if (!umbracoContentType.AllowedContentTypes.Select(x => x.Id.Value).OrderBy(x => x).SequenceEqual(allowedChildNodeList.OrderBy(x => x)))
                        {
                            umbracoContentType.AllowedContentTypes = allowedChildNodeList.Select(x => new ContentTypeSort { Id = new Lazy<int>(() => x) });
                            hasBeenModified = true;
                        }
                    }
                }

                // Templates
                if (contentTypeBase.IsAbstract)
                {
                    if (umbracoContentType.AllowedTemplates.Any())
                    {
                        umbracoContentType.AllowedTemplates = Enumerable.Empty<ITemplate>();
                        hasBeenModified = true;
                    }
                }
                else
                {
                    List<ITemplate> templates;
                    if (contentTypeAttribute.AllowedTemplates == null)
                    {
                        var codeFirstTemplateIds =
                            contentTypeTemplateProviders.SelectMany(
                                x => x.GetTemplateIdsForContentType(contentTypeBase)).ToList();
                        templates = codeFirstTemplateIds.Select(fileService.GetTemplate).ToList();
                    }
                    else
                    {
                        templates = new List<ITemplate>();
                        foreach (var allowedTemplateAlias in contentTypeAttribute.AllowedTemplates)
                        {
                            var template = fileService.GetTemplate(allowedTemplateAlias);
                            if (template == null)
                            {
                                throw new UCodeFirstException(Resources.UnableToFindTemplate.InvariantFormat(allowedTemplateAlias));
                            }

                            templates.Add(template);
                        }
                    }

                    if (!umbracoContentType.AllowedTemplates.Select(x => x.Id).OrderBy(x => x).SequenceEqual(templates.Select(x => x.Id).OrderBy(x => x)))
                    {
                        umbracoContentType.AllowedTemplates = templates;
                        hasBeenModified = true;
                    }
                }

                if (umbracoContentType.AllowedTemplates.Count() == 1)
                {
                    umbracoContentType.SetDefaultTemplate(umbracoContentType.AllowedTemplates.First());
                }

                if (hasBeenModified)
                {
                    contentTypeService.Save(umbracoContentType);
                }
            }
        }

        /// <summary>
        /// Holds temp information during synchronization.
        /// </summary>
        private class SynchroContentType
        {
            /// <summary>
            /// Gets or sets the content type base.
            /// </summary>
            public Type ContentTypeBase { get; set; }

            /// <summary>
            /// Gets or sets the Umbraco id.
            /// </summary>
            public int Id { get; set; }

            /// <summary>
            /// Gets or sets the document type (old API).
            /// </summary>
            public DocumentType DocumentType { get; set; }
        }

#pragma warning restore 612,618
    }
}
